package fr.loof.fonzie.parsers;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.loof.fonzie.keywords.Equals;

public class Property
    implements QueryElement
{
    private static Logger logger = LoggerFactory.getLogger( Property.class );

    private static KeyWord EQUALS = new Equals();

    private String name;

    private Class<?> entity;

    private Class<?> type;

    private final static Comparator<Field> FIELD_COMPARATOR = new Comparator<Field>()
    {
        public int compare( Field f1, Field f2 )
        {
            // longest name win
            return f2.getName().length() - f1.getName().length();
        }
    };;

    public Property( String name )
    {
        this.name = StringUtils.uncapitalize( name );
    }

    public String toString()
    {
        return name;
    }

    public void process( JPQLPersistenceQuery jpqlQuery, List<Class> params )
    {
        this.entity = jpqlQuery.getCurrentEntity();
        this.name = processPropertyPath( name, entity );
        if ( name == null )
        {
            throw new IllegalStateException( name + " is not a valid property path from entity "
                + entity.getSimpleName() );
        }

        jpqlQuery.append( " " )
                 .append( jpqlQuery.getCurrentEntityShortName() )
                 .append( "." )
                 .append( name );
        jpqlQuery.setCurrentProperty( this );
    }

    /**
     * @return <code>null</code> if the path doesn't match the object graph properties
     */
    private String processPropertyPath( String path, Class type )
    {
        logger.debug( "check property path {} on type {}", logger, type );
        String propertyPath = "";
        boolean found = false;
        for ( Field attribute : readAttributes( type ) )
        {
            String attrName = attribute.getName();
            if ( path.startsWith( attrName ) )
            {
                logger.trace( "attribute {} matches property path", attrName );
                propertyPath += attrName;
                path = path.substring( attrName.length() );
                this.type = attribute.getType();
                if ( path.length() > 0 )
                {
                    propertyPath += ".";
                    propertyPath += processPropertyPath(
                        StringUtils.uncapitalize( path ),
                        attribute.getType() );
                }
                found = true;
                break;
            }
        }
        if ( !found )
        {
            return null;
        }
        return propertyPath;
    }

    private ConcurrentMap<Class, List<Field>> attributesCache = new ConcurrentHashMap<Class, List<Field>>();

    protected List<Field> readAttributes( Class entity )
    {

        List<Field> attributes = attributesCache.get( entity );
        if ( attributes != null )
        {
            return attributes;
        }
        attributes = doReadAttributes( entity );
        attributesCache.putIfAbsent( entity, attributes );
        return attributes;
    }

    private List<Field> doReadAttributes( Class entity )
    {
        List<Field> attributes;
        attributes = new LinkedList<Field>();
        Class searchType = entity;
        while ( searchType.getAnnotation( Entity.class ) != null
            || searchType.getAnnotation( MappedSuperclass.class ) != null )
        {
            Field[] fields = searchType.getDeclaredFields();
            for ( int i = 0; i < fields.length; i++ )
            {
                Field field = fields[i];
                if ( Modifier.isTransient( field.getModifiers() ) || field.getAnnotation( Transient.class ) != null )
                {
                    continue;
                }
                attributes.add( field );
            }
            searchType = searchType.getSuperclass();
        }

        Collections.sort( attributes, FIELD_COMPARATOR );
        return attributes;
    }

    public boolean postProcess( ListIterator<QueryElement> elements )
    {
        if ( hasNoConstraint( elements ) )
        {
            elements.add( EQUALS );
        }
        return true;
    }

    private boolean hasNoConstraint( ListIterator<QueryElement> elements )
    {
        if ( !elements.hasNext() )
        {
            return true;
        }
        QueryElement next = elements.next();
        elements.previous();

        if ( next instanceof Constraint )
        {
            return false;
        }
        return true;
    }

    public final void checkParameterType( Class parameterType )
    {
        if ( !doCheckType( parameterType ) )
        {
            throw new IllegalStateException( "Constraint on " + entity.getSimpleName() + "." + name + " ("
                + type.getSimpleName() + ") is not compatible with parameter type " + parameterType );
        }
    }

    public boolean doCheckType( Class parameterType )
    {
        return parameterType.isAssignableFrom( type );
    }

    public String getName()
    {
        int dot = name.lastIndexOf( '.' );
        if ( dot > 0 )
        {
            return name.substring( dot + 1 );
        }
        return name;
    }

    public Class getEntity()
    {
        return entity;
    }

    public Class getType()
    {
        return type;
    }
}